import os
import sys
import imp
import inspect

class RuleSet(object):
    """
    This class represents a collection of rules.    
    """
    
    def __init__(self, path=None):
        '''Create RuleSet instance.

        path can be either a filename or the name of a module
        (eg: "rsa.rulesets.basic_ruleset")
        '''
        self.rules = []
        self.ignored = []
        self.name = ""
        self.description = ""

        if path is None: return

        if os.path.exists(path):
            self.load_from_file(path)
        else:
            # it's not a valid path; lets see if it's the name
            # of a package to be imported
            try:
                # N.B. __import__ returns the package (everything up
                # to the last dot) rather than the module. We have
                # to look up the module in sys.modules. Weird.
                __import__(path)
                module = sys.modules[path]
                self.load_from_module(module)
            except ImportError:
                pass
    
    def load_from_module(self, module):
        """ Load all rules in the given module """
        self.name = module.name
        self.description = module.description
        for _class in [ _class for _, _class in
                        inspect.getmembers(module, predicate=inspect.isclass) ]:
            if(self.is_rule(_class)):
                self.rules.append(_class())
        
    def load_from_file(self, path):
        """
        Load all rules in the given file
        """
        modulename = os.path.splitext(os.path.basename(path))[0]
        try:
            module = imp.load_source(modulename, path)
            self.load_from_module(module)
        except Exception, e:
            print "unable to load module:", e
            
    def is_rule(self, _class):
        """
        Check if the dynamically loaded class is an implementation of rsa.rule.Rule
        @param _class: The dynamically loaded class 
        """
        if(_class.__name__ == "TemplateRule"):
            return False
        _classes = inspect.getmro(_class)
        if(len(_classes) > 2):
            _rclass = _classes[len(_classes) - 2] 
            if(_rclass.__name__ == "Rule"):
                return True
        return False

    def add_rule(self, rule):
        """
        Add a new rule to this rule set
        @param rule the rule to be added
        """
        if(rule == None):
            raise Exception("Null Rule reference added to a RuleSet; that's a bug!")
        self.rules.append(rule)
        
    def get_rule(self, name):
        """
        Returns the Rule with the given name
        @param name the name of the rule to find        
        """
        for rule in self.rules:
            if(rule.name == name):
                return rule
        return None
    
    def apply(self, node, symbols):
        """
        Apply this rule set to the given test
        @param node: the test to apply the rule
        @param symbols: the symbols table of the entire system
        """
        for rule in self.rules:
            try:
                if(not rule.__class__.__name__ in self.ignored):
                    rule.apply(node, symbols)
            except Exception, e:
                raise Exception("[" + self.name + ":" + rule.name + "] " + str(e))    
            
    def exclude(self, rules):
        """
        Exclude the rules given as a parameter. The rules ignored will not be executed
        @param rule_name: the rule to be ignored 
        """
        for rule_name in rules:
            for rule in self.rules:
                if(rule.__class__.__name__ == rule_name):
                    self.rules.remove(rule)
    
    def include(self, rules):
        """
        Include the rules given as a parameter. Only this rules will be executed
        @param rules: the name of the rules to be used 
        """
        to_use = []
        for rule_name in rules:
            for rule in self.rules:
                if(rule.__class__.__name__ == rule_name):
                    to_use.append(rule)
                    break
        self.loaded = self.rules
        self.rules = to_use
        

    def ignore(self, rules):
        """
        Ignore in the next execution the rules given as parameter
        @param rules: the name of the rules to ignored
        """
        if(rules == None): return
        for rule in rules:
            if(not rule in self.ignored):
                self.ignored.append(rule)

    def consider(self, rules):
        """
        Remove from ignore list the rules given as parameter
        @param rules: the name of the rules to consider (removed from ignore list)
        """
        if(rules == None): return
        for rule in rules:
            if(rule in self.ignored):
                self.ignored.remove(rule)        
                        
    def __iter__(self):
        self._current = -1
        return self

    def next(self):
        self._current += 1
        if self._current < len(self.rules):
            return self.rules[self._current]
        else:
            raise StopIteration
            
class RuleSets(object):
    """
    Grouping of Rules per categories in a RuleSet.
    """
    
    def __init__(self):
        self.ruleSets = []

    def add_rule_set(self, ruleSet):
        if(ruleSet == None):
            raise Exception("Null Rule reference added to a RuleSet; that's a bug!")
        self.ruleSets.append(ruleSet)
        
    def apply(self, node, symbols):
        """
        Apply all applicable rules to test.
        """
        for ruleSet in self.ruleSets:
            ruleSet.apply(node, symbols)
    
    def __iter__(self):
        return self.ruleSets.__iter__()
    
    def exclude(self, rules):
        """
        Exclude the rules given as parameter. The rules ignored will not be executed
        """
        for ruleSet in self.ruleSets:
            if(ruleSet.name in rules):
                ruleSet.exclude(self.get_rules_names(ruleSet.rules))
            else:
                ruleSet.exclude(rules)
                
    def include(self, rules):
        """
        Include the rules given as parameter. Only this rules will be executed.
        """
        for ruleSet in self.ruleSets:
            if(ruleSet.name in rules):
                ruleSet.include(self.get_rules_names(ruleSet.rules))
            else:
                ruleSet.include(rules)
                
    def config(self, rules, option):
        """
        Sets the ignore list for future executions
        @param rules:  the rules to be ignored/considered
        @param option: indicates if the rules must be ignored o considered
        """
        for ruleSet in self.ruleSets:
            if(option == "ignore"):
                ruleSet.ignore(rules)
            if(option == "consider"):
                ruleSet.consider(rules)
                
    def get_rules_names(self, rules):
        """
        Get the class name of the rules given
        """
        names = []
        for rule in rules:
            names.append(rule.__class__.__name__)
        return names
